package sketch.ounit.fuzz;

import java.util.LinkedList;
import java.util.List;

import org.eclipse.jdt.core.dom.Modifier;

import randoop.ComponentManager;
import randoop.ExecutableSequence;
import randoop.ExecutionOutcome;
import randoop.NormalExecution;
import randoop.RConstructor;
import randoop.Sequence;
import randoop.Variable;
import randoop.util.ConstructorReflectionCode;
import randoop.util.Reflection;
import randoop.util.ReflectionCode;
import randoop.util.ReflectionExecutor;
import sketch.util.Checker;
import sketch.util.Globals;
import sketch.util.Randomness;
import sketch.util.RandoopUtil;

/**
 * Possible limitation of this class:
 * 1. it only invokes the constructor, in reality, a static factory or other method
 *    may also be able to create an object instance
 * */

public class AssembledSequenceObjectCreationWithArgs<T> extends AssembledSequenceObjectCreation<T> {

	//the argument objects
	public final Object[] args;
	//the string name of arguments
	private final String[] argNames;
	//the expression name of arguments
	private final String[] exprNames;
	
	//the interesting values for comparison
	private final RConstructor rconstructor;
	private final List<MethodArg> arg_list = new LinkedList<MethodArg>();
	
	//the created_obj
	private T created_obj = null;
	private Throwable exception = null;
	
	//construct an assembled sequence
	public AssembledSequenceObjectCreationWithArgs(Class<T> clazz, ComponentManager components, List<RConstructor> constructors,
			Object... args) {
		super(clazz, components);
		Checker.checkNull(args, "The argument could not be null.");
		Checker.checkNull(constructors, "the constructors could not be null.");
		Checker.checkTrue(!constructors.isEmpty(), "The constructor list could not be empty.");
		this.args = args;
		//randomly select a constructor
		this.rconstructor = Randomness.randomMember(constructors);
		//the default value of argument
		argNames = new String[args.length];
		for(int i = 0; i < argNames.length; i++) {
			argNames[i] = "%Arg_Name_" + i + "%";
		}
		exprNames = new String[args.length];
		for(int i = 0; i < exprNames.length; i++) {
			exprNames[i] = "%Expr_Name_" + i + "%";
		}
	}

	@Override
	public int size() {
		Checker.checkTrue(this.isValidSequence(), "The constructed sequence is not valid");
		int length = 0;
		for(MethodArg arg : this.arg_list) {
			if(arg.isExecutableSequence()) {
				length += arg.getESeq().sequence.size();
			}
		}
		//add the last one
		length++;
		return length;
	}
	
	public boolean hasException() {
		return this.exception != null;
	}
	
	public Throwable getException() {
		return this.exception;
	}
	
	@Override
	public T assemble_and_execute_sequence() {
		
		List<MethodArg> selected_args = new LinkedList<MethodArg>();
		//invoke the constructor, first find all argument
		List<Class<?>> inputTypes = this.rconstructor.getInputTypes();
		//System.out.println("input types: " + inputTypes);
		Object[] inputObjs = new Object[inputTypes.size()];
		//find input values for each argument
		int arg_index = 0;
		for(Class<?> type : inputTypes) {
			//find a set of type-compatible sequence
			//System.out.println("type: " + type);
			ExecutableSequence seq = super.getExecutableSequenceForType(type);
			//execute it
			List<Integer> compatible_objs = this.findAllCompatibleObjIndices(type);
			if(seq == null && compatible_objs.isEmpty()) {
				//the created obj is still null
				break;
			} else {
				boolean use_args = false;
				if(seq == null) {
					use_args = true;
				} else if (compatible_objs.isEmpty()) {
					use_args = false;
				} else {
					//use_args = Randomness.nextRandomBool() || Randomness.nextRandomBool();
					use_args = (Randomness.nextRandomInt(10) < AssembledSequence.use_arg_probability * 10);
				}
				//choose arg
				if(!use_args) {
					//use sequences
					seq.execute(null);
					//System.out.println("has non-executed: " + seq.hasNonExecutedStatements());
					List<Integer> indices = this.findCompatibleIndex(seq, type);
					Checker.checkTrue(!indices.isEmpty(), "The indices set could not be empty.");
					int index = Randomness.randomSetMember(indices);
					MethodArg arg = new MethodArg(seq, index);
					selected_args.add(arg);
					ExecutionOutcome outcome = seq.getAllResults()[index];
					Checker.checkTrue(outcome instanceof NormalExecution, "incorrect type: " + outcome.getClass());
					inputObjs[arg_index++] = ((NormalExecution)outcome).getRuntimeValue();
					//Checker.checkTrue(inputObjs[arg_index] instanceof , message)
				} else {
					//use provided args
					int selected_index = Randomness.randomMember(compatible_objs);
					MethodArg arg = new MethodArg(selected_index);
					selected_args.add(arg);
					inputObjs[arg_index++] = args[selected_index];
				}
			}
		}
		//find appropriate arguments
		if(selected_args.size() == inputTypes.size()) {
			//now execute the code
			ReflectionCode code = new ConstructorReflectionCode(this.rconstructor.getConstructor(), inputObjs);
			Throwable e = ReflectionExecutor.executeReflectionCode(code, System.out);
			if(e != null) {
				this.exception = e;
			} else {
				this.created_obj = (T) code.getReturnVariable();
				this.arg_list.addAll(selected_args);
			}
		} else {
			//does not create valid object
			Checker.checkTrue(this.created_obj == null, "Inconsistent state.");
		}
		
		//assign the created_object
		return this.created_obj;
	}
	
	private List<Integer> findAllCompatibleObjIndices(Class<?> type) {
		List<Integer> compatible_objs_index = new LinkedList<Integer>();
		for(int i = 0; i < args.length; i++) {
			Object obj = args[i];
			if(obj == null) {
				continue;
			} else {
				Class<?> obj_class = obj.getClass();
				if(!Modifier.isPublic(obj_class.getModifiers())) {
					continue;
				} else {
					if(Reflection.canBeUsedAs(obj_class, type)) {
						compatible_objs_index.add(i);
					}
				}
			}
		}
		return compatible_objs_index;
	}
	
	private List<Integer> findCompatibleIndex(ExecutableSequence sequence, Class<?> type) {
		ExecutableSequence eSeq = sequence;
		Checker.checkTrue(!sequence.hasNonExecutedStatements(), "The sequence should be executed");
		//check which index could be used
		Sequence seq = eSeq.sequence;
		List<Variable> lastVars = seq.getLastStatementVariables();
		List<Integer> all_compatibles = new LinkedList<Integer>();
		for(Variable var : lastVars) {
			Class<?> var_type = seq.getStatementKind(var.getDeclIndex()).getOutputType();
			if(Reflection.canBeUsedAs(var_type, type)) {
				all_compatibles.add(var.getDeclIndex());
			}
		}
		return all_compatibles;
	}
	
	@Override
	public boolean isValidSequence() {
		return this.created_obj != null;
	}
	
	@Override
	protected void checkSequenceValidity() {
	    Checker.checkTrue(this.isValidSequence(), "The sequence is not valid");	
	}
	
	@Override
	public String toCodeString(int offset, String receiver, String[] argnames, String[] expressionNames) {
		Checker.checkTrue(offset > -1, "The offset value is not valid: " + offset);
		Checker.checkNull(argnames, "The argument names should not be null.");
		Checker.checkTrue(this.argNames.length == argnames.length, "The required length : " + this.argNames.length
				+ ", the given length is: " + argnames.length);
		return this.toCodeString(offset, argnames, expressionNames);
	}
	
	@Override
	public String getResultVar(int offset) {
		//the result var is the last variable created
		//this method should be correlated with assemble_and_execute_sequence()
		this.checkSequenceValidity();
		int offset_size = offset;
		for(int i = 0; i < this.arg_list.size(); i++) {
			MethodArg arg = this.arg_list.get(i);
			if(arg.isExecutableSequence()) {
				offset_size += arg.getESeq().sequence.size();
			}
		}
		return Variable.getName(offset_size);
	}
	
	public void setOutputOffset(int offset, String[] argNames, String[] expressions) {
		super.setOutputOffset(offset);
		Checker.checkNull(argNames, "Argument names is not null.");
		Checker.checkTrue(argNames.length == this.argNames.length, "The length is not equal: "
				+ "given argnames: " + argNames.length + ", but requires: " + this.argNames.length);
		Checker.checkTrue(expressions.length == this.argNames.length, "The length is not equal: "
				+ "given argnames: " + expressions.length + ", but requires: " + this.argNames.length);
		for(int i = 0; i < argNames.length; i++) {
			Checker.checkNull(argNames[i], "The " + i + "-th name can not be null.");
			this.argNames[i] = argNames[i];
		}
		for(int i = 0; i < argNames.length; i++) {
			Checker.checkNull(expressions[i], "The " + i + "-th expression can not be null.");
			this.exprNames[i] = expressions[i];
		}
	}
	
	@Override
	public String toString() {
		return this.toCodeString();
	}
	
	public String toCodeString() {
		return this.toCodeString(super.offset, this.argNames, this.exprNames);
	}
	
	protected String toCodeString(int offset, String[] argNames, String[] expressionNames) {
		this.checkSequenceValidity();
		Checker.checkTrue(argNames.length == this.args.length, "Their lengths are not equal.");
		StringBuilder sb = new StringBuilder();
		
		String[] print_arg_names = new String[this.arg_list.size()];
		int offset_size = offset;
		for(int i = 0; i < this.arg_list.size(); i++) {
			MethodArg arg = this.arg_list.get(i);
			if(arg.isExecutableSequence()) {
				//XXX need to check
				RandoopUtil.appendSequenceWithOffset(arg.getESeq().sequence, offset_size, sb);
				Variable v = arg.getESeq().sequence.getVariable(arg.getESeqIndex());
				Variable newV = new Variable(v.sequence, v.index + offset_size);
				print_arg_names[i] = newV.toString();
				offset_size += arg.getESeq().sequence.size();
			} else if(arg.isPassedArg()) {
				int param_index = arg.getIndex();
				print_arg_names[i] = argNames[param_index];
				//print the declaration here
				sb.append("//declare var" + Globals.lineSep);
				sb.append("  " + this.rconstructor.getConstructor().getParameterTypes()[i].getName()
				               + " " + print_arg_names[i] + " = " + expressionNames[param_index] + ";");
				sb.append(Globals.lineSep);
			} else {
				Checker.checkNull(null, "wrong state.");
			}
		}
		
		//print the last constructor to create objects
		String varName = Variable.getName(offset_size);
		RandoopUtil.appendConstructor(this.rconstructor, varName, print_arg_names, sb);
		
		return sb.toString();
	}
	
	@Override
	public int hashCode() {
		int hashcode = 0;
		hashcode += 17*this.rconstructor.hashCode();
		for(MethodArg arg : this.arg_list) {
			hashcode += 7*arg.hashCode();
		}
		return hashcode;
	}
	
	@Override
	public boolean equals(Object obj) {
		if(!(obj instanceof AssembledSequenceObjectCreationWithArgs)) {
			return false;
		}
		AssembledSequenceObjectCreationWithArgs seq = (AssembledSequenceObjectCreationWithArgs)obj;
		if(!this.rconstructor.equals(seq.rconstructor)) {
			return false;
		}
		//check the arg_list
		List<MethodArg> args = seq.arg_list;
		return this.arg_list.equals(args);
	}
}